﻿// (c) Copyright Damian Mehers http://damianblog.com/
// This source is subject to the Microsoft Public License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.XPath;

namespace MceFM.Last.FM {
  /// <summary>
  /// Handles communication with Last.fm servers to request a new "station" based on an artist or tag, and to fetch the list of songs currently
  /// queued up
  /// </summary>
  internal class LastFm  {
    public enum PlayType {
      Artist,
      Tag
    };

    private const string HandshakeAddress =
      "http://ws.audioscrobbler.com/radio/handshake.php?version=1.3.1.1&platform=win32&username={0}&passwordmd5={1}&language={2}&player=wmp";

    private string basePath;
    private string baseUrl;
    private string currentTitle = "None";
    private string session;
    private WebClient webClient;

    #region ILastFm Members

    public bool IsInitialized() {
      return Session != null;
    }

    public void Initialize(string username, string passwordHash) {
      webClient = new WebClient();

      string address = String.Format(HandshakeAddress, username, passwordHash, "en");
      Dictionary<string, string> response = SendRequest(address);

      session = GetResponseValue(response, "session");

      if(Session == "FAILED") {
        session = null;
        string message =
          string.Format("Failed to logon to Last.fm: {0}",
                        response.ContainsKey("msg") ? response["msg"] : " Unknown error");
        Trace.TraceError(message);
        throw new Exception(message);
      }
      baseUrl = GetResponseValue(response, "base_url");
      basePath = GetResponseValue(response, "base_path");
    }

    public void Play(PlayType playType, string name) {
      if(Session == null) {
        Trace.TraceError("Internal error.  LastFm.Play called when not initialized");
        throw new Exception("Internal error.  LastFm.Play called when not initialized");
      }
      Play(playType == PlayType.Artist ? "artist" : "globaltags", name);
    }

    public string CurrentTitle {
      get {return currentTitle;}
    }

    public string Session {
      get { return session; }
    }

    public List<Track> FetchMoreTracks() {
      if(Session == null) {
        Trace.TraceError("Internal error.  LastFm.FetchMoreTracks called when not initialized");
        throw new Exception("Internal error.  LastFm.FetchMoreTracks called when not initialized");
      }

      string address = string.Format("{0}/xspf.php?sk={1}&discovery=0&desktop=1.3.1.1", GetBaseAddress(), Session);
      Trace.TraceInformation("Fetching more tracks by sending: {0}", address);
      string responseString;
      try {
        responseString = webClient.DownloadString(address);
      } catch(Exception ex) {
        Trace.TraceError("Unexpected error fetching more tracks from Last.fm: {0}", ex);
        throw new Exception("Unexpected error fetching more tracks from Last.fm:" + ex.Message, ex);
      }
      Trace.TraceInformation("Got response: {0}", responseString);

      List<Track> tracks = new List<Track>();

      try {
        XmlDocument xmlDocument = new XmlDocument();
        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
        namespaceManager.AddNamespace("lastfm", "http://www.audioscrobbler.net/dtd/xspf-lastfm");

        xmlDocument.LoadXml(responseString);
        XPathNavigator xpathNavigator = xmlDocument.CreateNavigator();


        XmlNode xmlNode = xmlDocument.SelectSingleNode("/playlist/title");
        if(xmlNode == null) {
          Trace.TraceError("Unexpected error fetching more tracks from Last.fm:  No playlist title");
          throw new Exception("Unexpected error fetching more tracks from Last.fm:  No playlist title");
        }

        currentTitle = HttpUtility.UrlDecode(xmlNode.InnerText);

        XPathNodeIterator iterator = xpathNavigator.Select("/playlist/trackList/track");
        while(iterator.MoveNext()) {
          Track track = new Track();
          track.Location = GetTrackField(iterator, "location");
          track.Title = GetTrackField(iterator, "title");
          track.Album = GetTrackField(iterator, "album");
          track.Creator = GetTrackField(iterator, "creator"); // +" (Last.fm:" + currentTitle + ")";
          track.Duration = int.Parse(GetTrackField(iterator, "duration"));
          track.Image = GetTrackField(iterator, "image");
          track.LastFmTrackAuth = GetTrackField(iterator, namespaceManager, "lastfm:trackauth");
          track.BuyUrl = GetTrackField(iterator, "link[@rel='http://www.last.fm/buyTrackURL']");
          tracks.Add(track);
        }
      } catch(XmlException xmlException) {
        Trace.TraceError("Unexpected error parsing response from Last.fm to request for more tracks: {0}", xmlException);
        throw new Exception(
          "There was an unexpected error when fetching the list of tracks from Last.fm:" + xmlException.Message,
          xmlException);
      }
      return tracks;
    }

    private static string GetTrackField(XPathNodeIterator iterator, IXmlNamespaceResolver namespaceManager, string field)
    {
      XPathNavigator node = iterator.Current.SelectSingleNode(field, namespaceManager);
      if (node == null)
      {
        Trace.TraceError("Unexpected error fetching more tracks from Last.fm:  No {0} in track", field);
        throw new Exception(string.Format("Unexpected error fetching more tracks from Last.fm:  No {0} in track", field));
      }
      return node.Value;
    }

    #endregion

    private static string GetTrackField(XPathNodeIterator iterator, string field) {
      XPathNavigator node = iterator.Current.SelectSingleNode(field);
      if(node == null) {
        Trace.TraceError("Unexpected error fetching more tracks from Last.fm:  No {0} in track", field);
        throw new Exception(string.Format("Unexpected error fetching more tracks from Last.fm:  No {0} in track", field));
      }
      return node.Value;
    }

    private void Play(string action, string value) {
      string address = GetAdjustAddress(action, value);
      Dictionary<string, string> response = SendRequest(address);
      string ok = GetResponseValue(response, "response");
      if(ok != "OK") {
        Trace.TraceError("Unexpected response from LastFM: {0}", ok);
        throw new Exception("Unexpected response from LastFM:" + ok);
      }
    }

    internal static string GetPasswordHash(string password) {
      MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
      UTF8Encoding encoder = new UTF8Encoding();
      byte[] hash = md5.ComputeHash(encoder.GetBytes(password));
      return Util.ToHexString(hash);
    }

    private Dictionary<string, string> SendRequest(string address) {
      string responseString;
      try {
        Trace.TraceInformation("Sending: {0}", address);
        responseString = webClient.DownloadString(address);
        Trace.TraceInformation("Got response: {0}", responseString);
        Trace.Flush();
      } catch(Exception ex) {
        Trace.TraceError("An unexpected error was returned from the Last.fm server: {0}", ex);
        throw new Exception("An unexpected error was returned from the Last.fm server: " + ex.Message, ex);
      }

      if(string.IsNullOrEmpty(responseString)) {
        Trace.TraceError("The response from the Last.fm was null or blank");
        throw new Exception("The response from the Last.fm was null or blank for the request " + address);
      }

      Dictionary<string, string> response = new Dictionary<string, string>();
      StringReader stringReader = new StringReader(responseString);
      for(string line = stringReader.ReadLine();line != null;line = stringReader.ReadLine()) {
        int firstEquals = line.IndexOf('=');
        if(firstEquals == -1) {
          Trace.TraceError("Unexpected formatted response from LastFM (missing an '=')");
          throw new Exception("Unexpected formatted response from LastFM");
        }
        response.Add(line.Substring(0, firstEquals), line.Substring(firstEquals + 1));
      }
      return response;
    }

    private static string GetResponseValue(IDictionary<string, string> response, string key) {
      if(!response.ContainsKey(key)) {
        Trace.TraceError("LastFM response is missing the {0}", key);
        throw new Exception(string.Format("LastFM response is missing the {0}", key));
      }
      return response[key];
    }


    private string GetBaseAddress() {
      return string.Format("http://{0}{1}", baseUrl, basePath);
    }

    private string GetAdjustAddress(string action, string value) {
      return string.Format("{0}/adjust.php?session={1}&url=lastfm://{2}/{3}", GetBaseAddress(), Session, action, value);
    }


  }
}